import fs from "fs";
import md5 from "md5";
import path from "path";
import xlsx from "node-xlsx";
import { BinaryWriteOp, BinaryWriter, IStructField, StructCoder, StructValueType as SVT } from "k-coder";

/**
 * Xlsx代码构建
 */
export class XlsxBuilder {

    private vos: IXlsxVo[] = []
    private maps: IMap[] = []
    private exportTyDic: { [ty: string]: boolean };

    constructor(
        /** 表格目录 */
        readonly inputDir: string,
        /** 代码输出目录 */
        readonly codeOutputDir: string,
        /** 数据输出目录 */
        readonly dataOutputDir: string,
        /** 导出类型 空为全导 */
        readonly exportTy?: string,
        /** 输出数据文件名 */
        readonly dataName = "data",
        /** 输出文件名 */
        readonly fileName = "Xlsx",
        /** 输出自动生成的定义代码名 */
        readonly autoDefName = "XlsxDef",
        /** 输出Xlsx对象名 */
        readonly objName = "Xlsx",
        /** 输出XlsxVo名 */
        readonly voName = "XlsxVo",
    ) {
        if (exportTy) {
            this.exportTyDic = {};
            exportTy.split(",").forEach(ty => this.exportTyDic[ty] = true);
        }
    }

    /** 构建表格对象  */
    build() {
        this.loadAllXlsx();
        this.codeOutputDir && this.buildCode();
        this.dataOutputDir && this.buildData();
    }

    /** 读取所有表格 */
    private loadAllXlsx() {
        //遍历表格目录及其子目录读取表格
        (function eachDir(dir: string, fn: (filePath: string, fileName: string) => void) {
            fs.readdirSync(dir).forEach(fileName => {
                let filePath = path.resolve(dir, fileName);
                if (fs.statSync(path.resolve(dir, fileName)).isDirectory()) {
                    //递归遍历子目录
                    eachDir(filePath, fn);
                } else {
                    fn(filePath, fileName);
                }
            })
        })(this.inputDir, (filePath, fileName) => {
            let match = fileName.match(/^([^~].*)\.xlsx$/);
            if (match) {
                //检查命名是否规范
                console.log(filePath);
                assert(!match[1].match(/[^_0-9a-zA-Z]/), `[${fileName}]命名不规范`);
                let file = fs.readFileSync(filePath);
                this.loadXlsx(file, match[1], filePath);
            }
        })
        //根据表名排序下
        this.vos.sort((a, b) => a.name.localeCompare(b.name));
    }

    /** 读取单个表格 */
    private loadXlsx(file: Buffer, fileName: string, filePath: string) {
        let table = xlsx.parse(file);
        assert(table[0], `${fileName}`);
        let data = table[0].data as any[][];
        let path = filePath.replace(this.inputDir, "").replace(/\\/g, "/").replace("/", "");
        let vo: IXlsxVo = {
            path,
            name: fileName,
            isCfg: data[0][0] == "#cfg",
            fields: [],
            data: [],
            md5: md5(file).slice(0, 8)
        }

        let maps: IMap[] = [];
        if (vo.isCfg) {
            //将cfg类型转成与普通表格一致的结构再统一处理
            let temp: any[][] = [["导出类型："], ["字段名称："], ["数据类型："], ["字段注释："], ["*"]];
            let i = 0;
            while (data[++i]) {
                if (data[i][0] == "*") {
                    for (let j = 0; j < temp.length; j++) {
                        temp[j].push(data[i][j + 1]);
                    }
                }
            }
            data = temp;
        }

        for (let i = 4; i < data.length; i++) {
            //第一列有*号标识才导出
            data[i][0] == "*" && vo.data.push(data[i]);
        }
        //1-导出类型 2-字段名称 3-数据类型 4-字段注释
        let [exportTys, fields, types, notes] = data;

        //根据导出类型，筛选出需要导出的字段
        let hasIdField = vo.isCfg ? true : false;
        for (let i = 1; i < exportTys.length; i++) {
            let exportTy: string = exportTys[i];
            if (!exportTy || (this.exportTy && !exportTy.split(",").some(ty => this.exportTyDic[ty]))) continue;
            let type: string = types[i];
            let field: string = fields[i];
            let note = notes[i];
            if (field.match(/#(map|enum|const enum)/)) {
                parseMap(field, note, type);
            } else {
                parseField(i, note, field, type);
                if (i == 1 && field === "id") hasIdField = true;
            }
        }
        assert(vo.fields.length == 0 || hasIdField, `[${fileName}.xlsx] >>> 第一列必须为id字段`);

        /** 解析字段 */
        function parseField(col: number, note: string, name: string, type: string) {
            assert(!name.match(/[^_0-9a-zA-Z]/), `[${fileName}.xlsx] >>> 字段<${name}>命名不规范`);
            let match = type.match(/([_0-9a-zA-Z]+)(?:\((\d+)\))*((?:\[\])+)*/);
            let assertStr = `[${fileName}.xlsx] >>> ${note}：数据类型<${type}>解析错误`;
            assert(match, assertStr);
            let svt: SVT = (SVT as any)[match[1]];
            assert(svt != undefined, assertStr);
            let arrStr = match[3] || "";
            let arrVec = Math.floor(arrStr.length / 2);
            let param = match[2] ? parseInt(match[2]) : 0;
            type = getTypeCode(svt, arrStr, assertStr);
            if (name == "id") {
                assert([SVT.int, SVT.uint, SVT.ulong, SVT.long, SVT.str], `[${fileName}.xlsx] >>> id字段的数据类型必须为 int|uint|long|ulong|str`)
            }

            vo.fields.push({
                name,
                col,
                note,
                type,
                svt,
                arrVec,
                structField: [name, svt, arrVec, param]
            });
        }

        /** 解析映射 */
        function parseMap(mapType: string, note: string, type: string) {
            let match = type.match(/([_0-9a-zA-Z]+),([_0-9a-zA-Z]+)+,([_0-9a-zA-Z]+)+,([_0-9a-zA-Z]+)+/);
            assert(match, `[${fileName}.xlsx] >>> ${note}：${mapType}<${type}>解析错误`);
            let name = match[1];
            let key = match[2];
            let val = match[3];
            let fieldNote = match[4];
            let map: IMap = {
                mapType,
                path,
                note,
                name,
                data: vo.data,
                noteCol: -1,
                keyCol: -1,
                valCol: -1,
            }
            fields.forEach((field, col) => {
                switch (field) {
                    case key:
                        map.keyCol = col;
                        break;
                    case val:
                        map.valCol = col;
                        break;
                    case fieldNote:
                        map.noteCol = col;
                        break;
                }
            })
            assert(map.keyCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${key}>字段未找到`);
            assert(map.valCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${val}>字段未找到`);
            assert(map.noteCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${fieldNote}>字段未找到`);
            maps.push(map);
        }

        /** 获取值类型代码 */
        function getTypeCode(svt: SVT, arrStr: string, assertStr: string) {
            let type = ""
            switch (svt) {
                case SVT.bool:
                    type = "boolean";
                    break;
                case SVT.uint:
                case SVT.int:
                case SVT.ulong:
                case SVT.long:
                case SVT.floatUint:
                case SVT.floatInt:
                case SVT.doubleUlong:
                case SVT.doubleLong:
                case SVT.doubleStr:
                    type = "number";
                    break;
                case SVT.str:
                    type = "string";
                    break;
                case SVT.obj:
                    type = "any";
                    break;
                case SVT.arr:
                    type = "any[]";
                    break;
            }
            if (!type) {
                assert(type, assertStr);
            }
            return type + arrStr;
        }
        if (vo.fields.length > 0 || maps.length > 0) {
            this.vos.push(vo);
            this.maps = this.maps.concat(maps);
        }
    }

    private buildCode() {
        let code = "//工具生成，请不要手动修改...\n";

        //写入kcoder初始化
        code += `let r: any, Coder: any, SVT: any;\n`;
        code += `/** 传入k-coder来进行初始化 */\n`;
        code += `export function initXlsx(kcoder: any) {\n`;
        code += `    r = new kcoder.BinaryReader();\n`;
        code += `    Coder = kcoder.StructCoder;\n`;
        code += `    SVT = kcoder.StructValueType;\n`;
        code += `}\n\n`;

        //写入读取表数据方法
        code += `/** 表数量 */\n`;
        code += `export const xlsxCount = ${this.vos.length};\n`;
        code += `/** 表对象 */\n`;
        code += `export const ${this.objName} = {} as I${this.objName};\n`;
        code += `/** 已加载的表 */\n`;
        code += `export const loadedDic: any = {};\n`;
        code += `const dataDic: any = {};\n`;
        code += `const clsDic: any = {};\n`;
        code += `/** 读取表数据 */\n`;
        code += `export function loadData(buf: Uint8Array, onCount?: (count: number) => void, onReload?: (reloadDic: { [name: string]: IXlsx[keyof IXlsx] }) => void) {\n`;
        code += `    let c = 0;\n`;
        code += `    r.start(buf);\n`;
        code += `    let reloadDic = Object.assign({}, loadedDic);\n`;
        code += `    while (r.hasNext) {\n`;
        code += `        let name = r.str;\n`;
        code += `        let md5 = r.str;\n`;
        code += `        let isCfg = r.byte;\n`;
        code += `        let data = r.cut(r.uint);\n`;
        code += `        let loaded = loadedDic[name];\n`;
        code += `        if (loaded && loaded.$md5 === md5) delete reloadDic[name];\n`;
        code += `        else {\n`;
        code += `            dataDic[name] = data;\n`;
        code += `            Object.defineProperty(Xlsx, name, {\n`;
        code += `                get: () => loadedDic[name] = (Object.defineProperty(Xlsx, name, {\n`;
        code += `                    value: load(name, md5, isCfg),\n`;
        code += `                    enumerable: true,\n`;
        code += `                    configurable: true\n`;
        code += `                }) as any)[name],\n`;
        code += `                enumerable: true,\n`;
        code += `                configurable: true\n`;
        code += `            });\n`;
        code += `        }\n`;
        code += `        onCount && onCount(++c);\n`;
        code += `    }\n`;
        code += `    for (let name in reloadDic) (Xlsx as any)[name];\n`;
        code += `    r.end();\n`;
        code += `    onReload && onReload(reloadDic);\n`;
        code += `}\n\n`;

        code += `function load(name: string, md5: string, isCfg: boolean) {\n`;
        code += `    r.start(dataDic[name]);\n`;
        code += `    delete dataDic[name];\n`;
        code += `    let Cls: any;\n`;
        code += `    if (!isCfg) {\n`;
        code += `        Cls = clsDic[name] ||= Function();\n`;
        code += `        Cls.$name = Cls.prototype.$name = name;\n`;
        code += `        Cls.$md5 = md5;\n`;
        code += `        Cls.mergeDic = {};\n`;
        code += `        Cls.vos = [];\n`;
        code += `        Cls.voDic ||= {};\n`;
        code += `        Cls.get ||= (id: any) => Cls.voDic[id];\n`;
        code += `    }\n`;
        code += `    let fieldLen = r.uint;\n`;
        code += `    let struct: any[] = [];\n`;
        code += `    let getId: any;\n`;
        code += `    let mergeCoders: any[] = [];\n`;
        code += `    for (let i = fieldLen; --i >= 0;) {\n`;
        code += `        let fieldName = r.str;\n`;
        code += `        let svt = r.byte;\n`;
        code += `        let arrVec = r.uint;\n`;
        code += `        let param = r.uint;\n`;
        code += `        if (isCfg) {\n`;
        code += `            struct.push([fieldName, svt, arrVec, param]);\n`;
        code += `        } else {\n`;
        code += `            if (fieldName === "id") {\n`;
        code += `                switch (svt) {\n`;
        code += `                    case SVT.int: getId = () => r.int; break;\n`;
        code += `                    case SVT.uint: getId = () => r.uint; break;\n`;
        code += `                    case SVT.long: getId = () => r.long; break;\n`;
        code += `                    case SVT.ulong: getId = () => r.ulong; break;\n`;
        code += `                    case SVT.str: getId = () => r.str; break;\n`;
        code += `                }\n`;
        code += `            } else if (svt < SVT.str && arrVec <= 0) {\n`;
        code += `                struct.push([fieldName, svt, arrVec, param]);\n`;
        code += `            } else {\n`;
        code += `                let merge: any = Cls.mergeDic[fieldName] = {};\n`;
        code += `                Object.defineProperty(Cls.prototype, fieldName, {\n`;
        code += `                    get: function () { return merge.dataArr[merge.idxArr[this.$idx]] },\n`;
        code += `                    enumerable: true,\n`;
        code += `                    configurable: true\n`;
        code += `                });\n`;
        code += `                mergeCoders.push({ fieldName, coder: new Coder([["dataArr", svt, arrVec + 1, param], ["idxArr", SVT.uint, 1]]) });\n`;
        code += `            }\n`;
        code += `        }\n`;
        code += `    }\n`;
        code += `    let coder = new Coder(struct);\n`;
        code += `    if (isCfg) {\n`;
        code += `        let vo = coder.decode(r);\n`;
        code += `        vo.$name = name;\n`;
        code += `        vo.$md5 = md5;\n`;
        code += `        return vo;\n`;
        code += `    }\n`;
        code += `    let voLen = r.uint;\n`;
        code += `    let voDic: any = {};\n`;
        code += `    for (let i = 0; i < voLen; ++i) {\n`;
        code += `        let id = getId();\n`;
        code += `        Cls.vos.push(voDic[id] = (Cls.voDic[id] || new Cls()));\n`;
        code += `        voDic[id].id = id;\n`;
        code += `        voDic[id].$idx = i;\n`;
        code += `        coder.decode(r, voDic[id]);\n`;
        code += `    }\n`;
        code += `    Cls.voDic = voDic;\n`;
        code += `    for (let i = 0; i < mergeCoders.length; ++i) {\n`;
        code += `        let { fieldName, coder } = mergeCoders[i];\n`;
        code += `        let merge = Cls.mergeDic[fieldName];\n`;
        code += `        coder.decode(r, merge);\n`;
        code += `        merge.dataArr.unshift(undefined);\n`;
        code += `    }\n`;
        code += `    return Cls;\n`;
        code += `}\n\n`;

        //写入表对象
        code += `export interface N {\n`;
        code += `    /** 表名 */\n`;
        code += `    readonly $name: string;\n`;
        code += `}\n\n`;

        code += `export interface Xlsx<T> extends N {\n`;
        code += `    /** 数据列表 */\n`;
        code += `    readonly vos: T[];\n`;
        code += `    /** 通过id获取行数据 */\n`;
        code += `    get: (id: number | string) => T;\n`;
        code += `}\n\n`;

        code += `export interface I${this.objName} {\n`;
        for (let vo of this.vos) {
            let voName = `${this.voName}.${vo.name}`;
            code += `    /** [${vo.path}] */\n`;
            code += `    readonly ${vo.name}: ${vo.isCfg ? voName : `Xlsx<${voName}>`};\n`;
        }
        code += `}\n\n`;

        //写入表vo对象
        code += `/** 表vo结构 */\n`
        code += `export namespace ${this.voName} {\n\n`
        for (let vo of this.vos) {
            code += `    /** [${vo.path}] */\n`;
            code += `    export interface ${vo.name} extends N {\n`;
            vo.fields.forEach(field => {
                let { note, name, type } = field;
                if (note) code += `        /** ${note} */\n`;
                code += `        readonly ${name}: ${type};\n`;
            })
            code += `    }\n\n`;
        };
        code += `}`;

        fs.writeFileSync(path.resolve(this.codeOutputDir, `${this.fileName}.ts`), code, { encoding: "utf-8", flag: "w+" });

        //写入输出自动生成的杂项代码
        code = "//工具生成，请不要手动修改...\n";
        for (let map of this.maps) {
            switch (map.mapType) {
                case "#map":
                    code += `\nconst _${map.name} = {\n`;
                    map.data.forEach(row => {
                        let val = row[map.valCol];
                        code += `    /** ${row[map.noteCol]} */\n`;
                        code += `    ${row[map.keyCol]}: ${isNaN(parseInt(val)) ? `"${val}"` : val},\n`;
                    })
                    code += `}\n`;
                    code += `/** ${map.note}[${map.path}] */\n`;
                    code += `export const ${map.name}:Readonly<typeof _${map.name}> = _${map.name}\n`;
                    break;
                case "#enum":
                case "#const enum":
                    code += `\n/** ${map.note}[${map.path}] */\n`;
                    code += `export ${map.mapType == "#const enum" ? "const " : ""}enum ${map.name} {\n`;
                    map.data.forEach(row => {
                        let val = row[map.valCol];
                        code += `    /** ${row[map.noteCol]} */\n`;
                        code += `    ${row[map.keyCol]} = ${isNaN(parseInt(val)) ? `"${val}"` : val},\n`;
                    })
                    code += `}\n`;
                    break;
            }
        }
        code = code.slice(0, code.length - 1);
        fs.writeFileSync(path.resolve(this.codeOutputDir, `${this.autoDefName}.ts`), code, { encoding: "utf-8", flag: "w+" });
    }

    private buildData() {
        let w = new BinaryWriter();
        for (let vo of this.vos) {
            let { name, md5, fields, data } = vo;
            //[写入] >>> str-表名 str-8位md5 byte-是否cfg
            w.str = name;
            w.str = md5;
            w.byte = vo.isCfg ? 1 : 0;
            let startLen = w.len;
            let tableLenOp = w.pushOp(BinaryWriteOp.uint, 0, 0);

            //[写入] >>> uint-字段数量
            w.uint = fields.length;

            /** 重复数据合并 数字类型不合并 */
            let mergeData: { [field: string]: { coder: StructCoder, dataArr: any[], idxArr: number[], idxDic: { [val: string]: number } } } = {};
            let struct: any = [];
            fields.forEach((field) => {
                //[写入] >>> 字段名
                let [fieldName, svt, arrVec, param] = field.structField;

                //[写入] >>> 数据结构 [str-字段名，uint-值类型 uint-数据维度 uint-float特殊参数]
                w.str = fieldName;
                w.byte = svt;
                w.uint = arrVec;
                w.uint = param as number;
                if (vo.isCfg) {
                    struct.push(field.structField);
                } else if (fieldName !== "id") {
                    if (svt < SVT.str && arrVec <= 0) {
                        struct.push(field.structField);
                    } else {
                        mergeData[fieldName] = {
                            coder: new StructCoder([["dataArr", svt, arrVec + 1, param], ["idxArr", SVT.uint, 1]]),
                            dataArr: [],
                            idxArr: [],
                            idxDic: {}
                        }
                    }
                }
            });

            //[写入] >>> 数据行数
            if (!vo.isCfg) w.uint = data.length;

            let coder = new StructCoder(struct);
            data.forEach(dataRow => {
                let obj: any = {};
                fields.forEach(field => {
                    let val = dataRow[field.col];
                    if (typeof val === "string") val = val.replaceAll("\\n", "\n");
                    if (field.svt == SVT.str) val = String(val);
                    else if (field.arrVec > 0 || field.svt >= SVT.obj) val = JSON.parse(val);
                    else if (!val) val = 0;

                    if (vo.isCfg) {
                        obj[field.name] = val;
                    } else {
                        if (field.name === "id") {
                            switch (field.svt) {
                                case SVT.int: w.int = val; break;
                                case SVT.uint: w.uint = val; break;
                                case SVT.long: w.long = val; break;
                                case SVT.ulong: w.ulong = val; break;
                                case SVT.str: w.str = val; break;
                            }
                        } else if (field.svt < SVT.str && field.arrVec <= 0) {
                            obj[field.name] = val;
                        } else {
                            let merge = mergeData[field.name];
                            if (val == undefined) {
                                //[写入] >>> 空值
                                merge.idxArr.push(0);
                            } else {
                                if (merge.idxDic[val]) {
                                    //[写入] >>> 值idx
                                    merge.idxArr.push(merge.idxDic[val]);
                                    return;
                                }
                                //[写入] >>> 值idx
                                let idx = merge.dataArr.push(val);
                                merge.idxArr.push(merge.idxDic[val] = idx);
                            }
                        }
                    }
                });
                coder.encode(obj, w);
            });

            if (!vo.isCfg) {
                //[写入] >>> 合并的数据
                fields.forEach(field => {
                    let data = mergeData[field.name];
                    data && data.coder.encode(data, w);
                });
            }

            //[写入] >>> uint-表数据长度
            let tabelLen = w.len - startLen;
            tableLenOp.v = tabelLen;
            w.len += w.getUintLen(tabelLen);
        }

        //输出data文件
        fs.writeFileSync(path.resolve(this.dataOutputDir, `${this.dataName}.bin`), w.end(), { flag: "w+" });
    }
}

/** 断言失败退出程序并输出错误信息 */
function assert(cond: any, msg?: string) {
    if (!cond) {
        console.log(msg);
        process.exit();
    }
    return cond;
}

interface IField {
    name: string
    col: number,
    note: string,
    type: string,
    svt: SVT,
    arrVec: number,
    structField: IStructField
}

interface IXlsxVo {
    path: string,
    name: string,
    isCfg: boolean,
    fields: IField[],
    data: any[],
    md5: string,
}

interface IMap {
    mapType: string,
    path: string,
    note: string,
    name: string,
    noteCol: number,
    keyCol: number,
    valCol: number
    data: any[],
}